Storage and Encryption
Use toolkit:kv-ksafe for key-value data and persistent secrets, data:database-delight for SQL storage,
and toolkit:crypto for HPKE string encryption. Add the relevant module to the consumer's
commonMain.dependencies; the app already includes these modules.
Store wrappers such as PlaygroundKvStore and DbSecretStore must use @SingleIn(AppScope::class)
with the current implementation. Each wrapper instance calls factory.create(...), creating another
KSafe instance. App-scoped wrappers ensure reuse; KvStore and SecretStore remain unscoped factory products.
Key-Value Storage
Inject KvStoreFactory and create a named store. Names must match [a-z][a-z0-9_]*; reuse the same
name to reopen data. Values can be primitives or Kotlin-serializable types. Keep each key's type consistent.
kotlin1import dev.zacsweers.metro.AppScope2import dev.zacsweers.metro.Inject3import dev.zacsweers.metro.SingleIn4import io.baselines.sample.toolkit.kv.KvStore5import io.baselines.sample.toolkit.kv.KvStoreFactory67@Inject8@SingleIn(AppScope::class)9class SettingsStore(factory: KvStoreFactory) {10 private val store = factory.create("settings", KvStore.Mode.Encrypted)1112 fun observeTheme() = store.observe<String>("theme")13 suspend fun readTheme(): String? = store.read<String>("theme")14 suspend fun saveTheme(theme: String) = store.put("theme", theme)15 suspend fun removeTheme() = store.remove("theme")16}
Use your project's namespace in place of io.baselines.sample.
KvStore.Mode | Protection |
|---|---|
Plain | No app-level encryption |
Encrypted | Encryption using platform-protected keys |
HardwareIsolated | Prefers dedicated security hardware; falls back to Encrypted |
The mode applies to subsequent writes. Changing it does not rewrite existing entries; reads use each entry's stored mode.
read<T>()returnsnullfor missing entries, stored nulls, or values that cannot be decoded. Temporary decryption failures throw.observe<T>()emits the current value and distinct changes, retrying temporary decryption failures.contains()checks presence, including null or unreadable entries; it may block on initial load.put(),remove(), andclear()suspend until persistence completes. Writing null does not remove an entry;clear()removes every entry in that named store.
Persistent Secrets and Database Encryption
Inject SecretStoreFactory and call create(name) to open an encrypted secret store, using the same
store-name rules as KvStore. getOrCreate(key) accepts a nonblank key and returns its existing
32-byte secret, creating one only when absent. An unreadable stored secret throws instead of being
replaced.
data:database-delight enables SQLCipher encryption through DatabaseModule.DB_ENCRYPTED, which
defaults to true. DbSecretStore stores passphrases in db_secrets_v1, keyed by database name.
The SQL drivers consume these secrets directly; they do not use HpkeCipher.
DbSecretStore blocks while accessing secrets, so keep direct calls off the UI thread.
String Encryption
Inject HpkeCipher from io.baselines.sample.toolkit.hpke. Its suspend methods use Signum-backed HPKE:
kotlin1// In a suspend caller with an injected HpkeCipher:2val encrypted = hpkeCipher.encrypt("Example", HpkeKey.Playground)3val decrypted = hpkeCipher.decrypt(encrypted, HpkeKey.Playground)
HpkeKey is in the same package. Add a separate entry with a stable alias for each independent
encryption purpose. Encryption creates the selected recipient key if absent. Decryption requires
the original key and never creates a replacement; losing it makes existing ciphertext unreadable.
Keys use platform storage with preferred hardware backing when key agreement is supported. Otherwise, the implementation persists a software key in an encrypted KSafe store. Hardware backing is not guaranteed. Treat ciphertext as opaque and retain it unchanged.
For existing apps, follow the storage migration notes.